#!/usr/bin/env python3

""" Hydra Paste
    A libre paste engine with lots of power.

    Licensed under the GNU Affero Public License Version 3
        or Later. See LICENSES/LICENSE-AGPL for details.
    (c) Copyright 2015 Hydra Group
"""

from bottle import run, template, Bottle, request, response, static_file, abort, redirect, BaseRequest
import dataset   # for database management
from hashlib import md5, sha512  # for authentication
import os        # for file paths
import time      # for Expires and Last-Modified
from magic import from_file
import string
from uuid import uuid4              # for pseudorandom number generation
from random import SystemRandom     # for file IDs and captcha generation
from threading import Thread  # for file deletion
from contextlib import contextmanager

from sys import path, getsizeof
path.insert(1, 'lib/')

import safeurl
from skimpyGimpy import skimpyAPI


VERSION = '1.0.4-dev'

ui = Bottle()
ui.config.load_config('hp.ini')

ROOT = ui.config['paste.target_dir']
DIR_TEMPLATES = ui.config['paste.templates_dir']
DIR_FILES = ui.config['paste.files_dir']

ALERT = ui.config['paste.alert']

# MEMFILE_MAX change for larger text pastes
BaseRequest.MEMFILE_MAX = eval(ui.config['memfile_max'])

ALLOWED_CHARS = string.ascii_letters + string.digits + '_.-'

CLEAN_INTERVAL = int(ui.config['paste.clean_interval'])

db = None

SECRAND = SystemRandom()  # generator of randomness
CAPTCHA_SESSIONS = []  # captcha cookies

# TODO: Add error pages for API, so we don't server HTML to API
def custom_error(error):
    return gen_page(os.path.join(DIR_TEMPLATES, 'error.html'),
                    dict(code=error.status_code, status_line=error.status_line,
                         error_text=error.body, trace=error.traceback,
                         exception=error.exception))

ERROR_CODES = (400, 401, 402, 403, 404, 405, 406, 406, 407, 408, 409, 410, 411,
               412, 413, 414, 415, 416, 417, 418, 419, 420, 431, 500, 501, 502,
               503, 504, 505, 511)
CUSTOM_ERROR_HANDLER = dict()

for code in ERROR_CODES:
    CUSTOM_ERROR_HANDLER[code] = custom_error

ui.error_handler = CUSTOM_ERROR_HANDLER

# Stuff to show latest revision
def get_git_revision_hash():
    import subprocess
    return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')

GIT_HASH_LONG = get_git_revision_hash()
GIT_HASH = GIT_HASH_LONG[:6]
GIT_REPO = ui.config['git.repo']
#print('GIT_REPO {}'.format(GIT_REPO))
#print('GIT_HASH {}'.format(GIT_HASH))
GIT_COMMIT_URL = str(ui.config['git.commit_url']).replace('$REPO', GIT_REPO).replace('$HASH', GIT_HASH_LONG)

#######################################
# Login, sessions and shit            #
#######################################


def get_login_creds():
    """
    Get the username/password combination from a request

    Used for discovering credentials from API from POST
        data or from the `Authorization` HTTP Header.

    Returns a tuple in the format (username, password)
    """
    # TODO: Improve robustness of this code
    if request.method == 'POST':
        username = request.forms.getunicode('username')
        password = request.forms.getunicode('password')

        return (username, password)
    else:
        if 'Authorization' in request.headers:
            try:
                _method, user_pass_combo = request.headers['Authorization'].split()
                username, password = user_pass_combo.split(':')
                return (username, password)
            except ValueError:
                abort(400, 'Bad login format. Refer to API documentation for correct format.')


def check_login(usr, pwd, anon=True):
    """
    Check for password login, or anonymous login.

    `usr` - username for login to check
    `pwd` - password for login to check
    `anon` - whether or not to allow anonymous logins

    Returns: `True` if login is good, `False` if otherwise.
    """
    if usr.lower() == 'anonymous':  # Anonymous login
        return anon
    else:
        result = db['users'].find_one(username=usr)

        if result and pwd:  # login exists
            salt_hash_comb = result['p_hash']

            # cipher_num is for future migrations/logins
            # 6 = sha-512
            # pbkdf2-XXXX = pbkdf2 with XXXX iterations
            cipher_num, salt, login_hash = salt_hash_comb.split('$')

            calc_hash = None

            if cipher_num == '6':
                calc_hash = sha512(bytes(salt + pwd, 'utf-8')).hexdigest()

            if calc_hash == login_hash:
                return True

    return False


def check_login_session():
    """
    Check that a user's session is good by looking at their cookies.

    Cookies are generated in start_login_session()
    Cookies are removed in end_login_session()

    Returns the user's username if they're logged in
    """
    login_session = request.get_cookie('login_session')

    if login_session and ':' in login_session:
        username, session = [x for x in login_session.split(':', 1)]
        user_row = db['session'].find_one(user=username, session=session)

        if user_row:
            match_id = '{}:{}'.format(user_row['user'], user_row['session'])
            if match_id == login_session:
                return username
        else:  # Expired session
            end_login_session(username, forced=True)

    return False


def remove_session(username):
    """
    Search for a login session by username and remove it.

    For use with internal API only.

    `username` - `username` of session to remove
    """

    results = db['session'].find(user=username)

    db.begin()
    for row in results:
        row_id = row['id']
        db['session'].delete(id=row_id)
    db.commit()


def start_login_session(usr, remember=False):
    """
    Gives a user a cookie to start their login session

    `usr` - The user that will get a cookie
    `remember` - Whether to have the login session "remembered"
    """
    # Find session for user in database and remove it
    remove_session(username=usr)

    # Format of the session cookie is `user:session_id`
    session_id = sha512(bytes(uuid4().hex, 'utf-8')).hexdigest()
    session_cookie = '{}:{}'.format(usr, session_id)

    # Add session to database
    db.begin()
    db['session'].insert({'user': usr, 'session': session_id})
    db.commit()

    if remember:
        expire_time = time.time() + 14 * 24 * 3600  # Expire in 14 days
        response.set_cookie('login_session', session_cookie, expires=expire_time)
    else:
        response.set_cookie('login_session', session_cookie)


def _get_session_username():
    """
    Get the username of the current session in the request

    !!! NOTE: This DOES NOT verify the user is logged in, and should
    be used for cosmetic purposes only. !!!

    Returns: `username` of the user with a cookie (?)
    """
    login_session = request.get_cookie('login_session')

    if login_session and ':' in login_session:
        return login_session.split(':', 1)[0]
    else:
        return None


def check_captcha(cookie_id, captcha_answer):
    """
    Check a captcha answer and cookie ID against stored credentials

    `cookie_id` - Unique ID of a session
    `captcha_answer` - Answer returned by the user

    Returns: `True` if the answer matches the known solution
    """
    for auth_pair in CAPTCHA_SESSIONS:
        if auth_pair[0] == cookie_id and auth_pair[1] == captcha_answer:
            CAPTCHA_SESSIONS.remove(auth_pair)  # Remove correctly solved session, we don't need it anymore
            return True

    return False


def require_web_login(cb):
    """
    Decorator to require logins for certain pages

    Provides a `username` variable in the function it decorates.
    This decorator guarantees that the user is logged in.
    Moreover, it causes a user whom is not logged in to be
        redirected to the login page.
    """
    def auth(*args, **kwargs):
        username = check_login_session()
        if not username:
            redirect('/login?redirect={}'.format(request.path))
        else:
            return cb(*args, username=username, **kwargs)

    return auth


@contextmanager
def api_login(anonymous=False, match_username=None):
    """
    Context manager to require API logins for certain APIs.

    Returns the username of the currently logged in user.

    anonymous - whether or not we will allow anonymous users
    match_username - force a user's username to match a certain
        one, otherwise we shall return a 401 error
    """
    username, password = get_login_creds()

    if anonymous and (username == None or username == ''):
        username = 'anonymous'
    elif username == '' or username == None:
        abort(401, 'You must login for this API')

    if password == None:
        password = ''

    if username.strip() == '':
        abort(401, 'You must specify a login to use this API call.')
    elif not check_login(username, password, anon=anonymous):
        abort(401, 'That username/password combination is not recognized.')
    elif username.lower() == 'anonymous' and not anonymous:
        abort(401, 'You must have an account to do this.')
    elif match_username and username.lower() != match_username.lower():
        abort(401, 'Your account does not have permission to do that!')
    else:
        yield username


#######################################
# General functions                   #
#######################################


def generate_etag(param_list):
    """
    Generate a weak ETag from a list of parameters.

    ETAG is generated from concatenating items in param_list,
    and hashing the result through an MD5 Hash.

    MD5 is used because it is fast. In this case, the ETag does
    not have to be secure, so using MD5 is not an issue.

    `param_list` - List of things to be hashed into an ETag

    Returns: ETag of `param_list`
    """
    combo = ''

    for param in param_list:
        combo += str(param)

    # 'W/' for weak ETags
    return 'W/' + md5(bytes(combo, 'utf-8')).hexdigest()


def get_random_key():
    """
    Get a file key that isn't already taken

    Returns: an unique key
    """
    key = SECRAND.randint(0, 66 ** 4)

    while True:
        if not db['files'].find_one(key=key):
            return key
        key = SECRAND.randint(0, 66 ** 4)  # O(1) operation


def sanitize(unsafe_input, allowed=ALLOWED_CHARS):
    """
    Check for unsafe input

    Returns True if sanitary input, False if not.
    """
    for character in unsafe_input:
        if character not in allowed:
            return False
    return True


def user_pastes(username):
    """
    Return a dicitonary of a user's pastes

    username - the user to get pastes from
    """
    search_results = db['files'].find(user=username)
    data = {}

    for paste in search_results:
        key = paste['key']
        short_id = safeurl.num_encode(key)

        data[short_id] = api_file_info(short_id)

    return data


def all_pastes():
    """
    Return a dictionary of all pastes
    """
    all_pastes = db['files']

    data = {}

    for paste in all_pastes:
        key = paste['key']
        short_id = safeurl.num_encode(key)

        data[short_id] = api_file_info(short_id)

    return data


def sizeof_fmt(num, suffix='B'):
    """
    Human readable size format from bytes
    From http://stackoverflow.com/a/1094933
    Credit - Fred Cirera
    """
    for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
        if abs(num) < 1024.0:
            return "%3.1f%s%s" % (num, unit, suffix)
        num /= 1024.0
    return "%.1f%s%s" % (num, 'Yi', suffix)

#######################################
# Files and shit                      #
#######################################


def get_file(fileid, mime_type, download=False, filename=None):
    """
    Get a file

    `fileid` - The unique ID of a file
    `mime_type` - Used to identify a file's type
    `download` - Force download of a file
    `filename` - Original name of a file

    Returns: File
    """
    file_headers = api_file_info(fileid, headers=True)
    response = None

    if download:
        response = static_file(fileid, root=DIR_FILES, mimetype=mime_type, download=filename)
    else:
        response = static_file(fileid, root=DIR_FILES, mimetype=mime_type)

    for header, value in file_headers.items():
        response.set_header(header, value)

    return response


def increment_view_count(paste):
    """
    Increment the view counter of a paste by 1.

    `paste` - the short key of a paste
    """
    long_key = safeurl.num_decode(paste)

    row_file = db['files'].find_one(key=long_key)

    db.begin()
    if row_file['user'].lower() != 'anonymous':
        row_user = db['users'].find_one(username=row_file['user'])
        db['users'].update({'username': row_file['user'], 'views': (row_user['views'] + 1)}, ['username'])

    db['files'].update({'key': row_file['key'], 'views': (row_file['views'] + 1)}, ['key'])
    db.commit()


def modify_paste_stats(username, delta):
    """
    Change the number of pastes a user has, as well as the delta size change in bytes.

    username - the username of the user
    delta - the amount of bytes to add to the users' pastes
    """
    row = db['users'].find_one(username=username)

    if delta > 0:
        db['users'].update({'username': username,
                            'bytes': (row['bytes'] + delta),
                            'totalpastes': (row['totalpastes'] + 1),
                            'activepastes': (row['activepastes'] + 1)},
                           ['username'])
    elif delta < 0:
        db['users'].update({'username': username,
                            'bytes': (row['bytes'] + delta),  # Adding a negative = subtraction
                            'activepastes': (row['activepastes'] - 1)},
                           ['username'])
    else:
        abort(500, """While modifying the user's paste stats, we didn't know if """
                   """we were adding a paste or subtracting it!""")


def delete_paste(fileid):
    """
    Delete a paste and modify the appropriate account meta data.
    """
    longkey = safeurl.num_decode(fileid)

    file_row = db['files'].find_one(key=longkey)

    if not file_row:
        abort(404, 'Paste {} doesn\'t exist.'.format(fileid))

    user = file_row['user']
    if user.lower() != 'anonymous':
        delta = int(file_row['filesize'])
        modify_paste_stats(user, (-1 * delta))
    try:
        os.remove(os.path.join(DIR_FILES, fileid))
        db.begin()
        db['files'].delete(key=longkey)
        db.commit()
    except FileNotFoundError:
        print('[DBG] Unable to delete paste {}'.format(fileid))
        abort(500, """We weren't able to physically delete the paste...\n"""
                   """We need more firepower.  And dynamite.""")
    print('[DBG] Deleted paste: {}'.format(fileid))


#######################################
# API Stuff                           #
#######################################


@ui.post(path=('/api/user', '/api/user/'))
@ui.route('/api/user')
def api_user_pastes():
    """
    API for viewing a user's pastes.
    User must be logged in to see own pastes.

    Returns: Data if the user is logged in, otherwise error 401
    """
    with api_login() as user:
        return user_pastes(user)


@ui.delete('/api/file/<fileid>')  # REST API
@ui.post('/api/file/rm/<fileid>')  # Pleb API
def api_file_delete(fileid):
    """
    Delete a file by its file id.

    Authorization is in the user-pass format.
    """
    file_info = api_file_info(fileid)

    with api_login(anonymous=False, match_username=file_info['user']) as user:
        delete_paste(fileid)
        return 'Paste {} deleted!'.format(fileid)


@ui.route(path=('/api/file/<fileid>', '/api/file/get/<fileid>'))  # TODO: Depreciate /api/file/get/<fileid>
def api_get_file(fileid):
    """
    Retrieve information about a file or download it.

    #: `GET`  - Downloads file `fileid`
    #: `HEAD` - Downloads information about a file from it's headers.
    """
    if not sanitize(fileid, safeurl.ALPHABET):
        abort(404, 'Invalid paste key (bad formatting).')

    real_key = safeurl.num_decode(fileid)
    row = db['files'].find_one(key=real_key)  # At this point, `fileid` is safe
    if row:
        # Check if referer is web UI
        if 'Referer' in request.headers and \
           'paste.hydra.ws' in request.headers['Referer']:
            pass
        elif request.method == 'GET':
            increment_view_count(fileid)

        mime_type = row['filetype']

        return get_file(fileid, mime_type)
    else:
        abort(404, 'Invalid paste key (paste "{}" doesn\'t exist).'.format(fileid))


@ui.route('/api/file/info/<fileid>')
def api_file_info(fileid: str, headers=False):
    """
    Get information about a file

    `fileid` - Unique file ID
    `headers` - To send custom headers or not

    Returns: Information about `fileid`, error 404 otherwise
    """

    if not sanitize(fileid, safeurl.ALPHABET):
        abort(404, 'Invalid paste key (bad characters).')

    file_key = safeurl.num_decode(fileid)

    db_result = db['files'].find_one(key=file_key)

    if db_result and headers:
        # Create ETag for file
        last_modified = db_result['time']
        filename = db_result['filename']
        etag = generate_etag(('static-', filename, last_modified))

        return {'X-Paste-User': db_result['user'],
                'X-Paste-Expires': db_result['expires'],
                'X-Paste-Filename': filename,
                'X-Paste-Title': db_result['title'],
                'X-Paste-Views': db_result['views'],
                'Content-Type': db_result['filetype'],
                'Content-Length': db_result['filesize'],
                'Cache-Control': 'public, max-age=604800',  # Cache for 7 days
                'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime(last_modified)),
                'ETag': etag}
    elif db_result and not headers:
        return {'user': db_result['user'],
                'expires': int(db_result['expires']),
                'filename': db_result['filename'],
                'filetype': db_result['filetype'],
                'filesize': int(db_result['filesize']),
                'title': db_result['title'],
                'time': db_result['time'],
                'views': db_result['views'],
                'key': fileid}
    else:
        abort(404, 'Invalid paste key (paste "{}" doesn\'t exist).'.format(fileid))


@ui.post('/api/file/<fileid>')  # For file updates
@ui.post('/api/upload')  # For initial uploads
def api_upload(fileid=None, web_ui=False, username=None):
    """
    Function for uploading files to server.
    Allows option to modify existing file if uploaded from personal account.

    fileid - if non-blank, the short ID of the file to upload
    web_ui - whether this upload was via the web ui - assumes correct authentication
    username - webui username
    """
    if not web_ui:
        with api_login(anonymous=True) as user:
            username = user

    file_text = request.forms.getunicode('text', default=None)

    # File upload information
    upload_time = int(time.time())
    upload = request.files.get('upload')
    upload_size = None
    file_name = None
    if upload:
        upload_size = request.content_length  # For internal purposes, this
        # is a rough hack because bottle's FileUpload.content_length is broken
        file_name = upload.filename
    else:
        upload_size = getsizeof(file_text)

    if not (file_text or upload):
        abort(400, 'You need to paste something!')

    # Size + 400 because of metadata included in request header
    if file_text and upload_size >= 10000000 + 400:
        abort(413, 'You may only upload text up to 10 MB long.')
    elif not file_text and upload_size >= 150000000 + 400:
        abort(413, 'You may only upload files up to 150 MB large.')

    expires = None

    try:
        expires = request.forms.get('expires')

        delta_time = None

        if '+' in expires:  # If delta time
            delta_time = int(expires.strip()[1:])

            expires = int(time.time()) + delta_time

        else:  # If manual time time
            if expires is None:  # not specified, give default time
                expires = int(time.time()) + 518400  # 518400 sec = 6 days
            elif int(expires) == 0:  # Never expires
                expires = 0
            else:  # Give custom time
                expires = int(expires)
    except:
        abort(400, 'The "expires" parameter was messed up. Please refer to the API for correct usage.')

    # Paste title
    paste_title = request.forms.getunicode('title') or ''
    if paste_title.strip() == '':
        paste_title = file_name if file_name else 'Untitled'

    if fileid:  # If updating an existing file
        file_info = api_file_info(fileid)
        with api_login(match_username=file_info['user']) as user:
            save_path = os.path.join(DIR_FILES, fileid)
            orig_size = file_info['filesize']

            if upload:
                upload.save(save_path, overwrite=True)
            else:
                with open(save_path, 'wb') as save_file:
                    save_file.write(bytes(file_text, 'utf-8'))

            filesize = os.stat(save_path).st_size

            file_info.update(time=int(time.time()),
                             filesize=int(filesize),
                             title=paste_title,
                             expires=int(expires),
                             key=int(safeurl.num_decode(fileid)))

            db.begin()
            db['files'].update(file_info, ['key'])
            db.commit()

            modify_paste_stats(user, int(filesize - orig_size))

            return 'Success! File Modified: {}'.format(fileid)

    key = get_random_key()
    short_key = safeurl.num_encode(key)

    # Add file to database
    db.begin()
    db['files'].insert({'user': username,
                        'expires': int(expires),  # If 0, never expires. If defined, expires @ epoch time
                        'filename': file_name,
                        'filetype': None,
                        'filesize': int(0),
                        'time': upload_time,
                        'views': 0,
                        'key': key,
                        'title': paste_title})

    save_path = os.path.join(DIR_FILES, short_key)
    if upload:
        upload.save(save_path)
    else:
        with open(save_path, 'wb') as save_file:
            save_file.write(bytes(file_text, 'utf-8'))

    # Detect Mime or get paste
    mime_type = request.forms.getunicode('mimetype')
    if not mime_type or mime_type == 'auto':
        mime_type = from_file(save_path, mime=True).decode('utf-8')

    row_id = db['files'].find_one(key=key)['id']
    filesize = int(os.stat(save_path).st_size)

    db['files'].update({'id': row_id, 'filetype': mime_type, 'filesize': filesize}, ['id'])
    db.commit()

    if username.lower() != 'anonymous':
        modify_paste_stats(username, filesize)

    return 'Success! File ID: {}'.format(short_key)


#######################################
# Web UI                              #
#######################################


@ui.route('/user')
@ui.route('/user/')
@require_web_login
def ui_user_dashboard(username):
    """
    Page to show a user their account overview.
    """
    row = db['users'].find_one(username=username)
    data = {'username': username,
            'numviews': row['views'],
            'numbytes': sizeof_fmt(row['bytes']),
            'numactive': row['activepastes'],
            'numtotal': row['totalpastes'],
            'joined': row['joined']}

    return gen_page(os.path.join(DIR_TEMPLATES, 'dashboard.html'), data)


@ui.route('/user/pastes')
@require_web_login
def ui_user_pastes(username):
    """
    Page to show a user their pastes.
    """
    pastes = user_pastes(username)

    return gen_page(os.path.join(DIR_TEMPLATES, 'userpastes.html'),
                    {'pastes': pastes,
                     'username': username})

@ui.route('/admin')
@require_web_login
def admin_page(username):
    """
    Admin control panel
    """
    if username != ui.config['paste.admin']:
        abort(401, 'You need to be admin to do that, faggot.')
    else:
        pastes = all_pastes()
        return gen_page(os.path.join(DIR_TEMPLATES, 'userpastes.html'),
                        dict(username=username, admin_view=True, pastes=pastes))


@ui.route('/user/changepass')
@ui.post('/user/changepass')
@require_web_login
def user_change_password(username):
    """
    Change a users's password
    """
    data = {'error': None,
            'success': None,
            'username': username}

    if request.method == 'POST':
        current = request.forms.getunicode('pass-current')
        pass1 = request.forms.getunicode('pass-new')
        pass2 = request.forms.getunicode('pass-confirm')

        if not check_login(username, current, anon=False):
            data['error'] = 'Incorrect current password. Please try again.'
        elif pass1 != pass2:
            data['error'] = 'Passwords don\'t match. Please try again.'
        elif pass1 == current:
            data['error'] = 'Current and new password match'
        elif len(pass1) <= 6:
            data['error'] = 'Password too weak. Try using more than 6 characters.'
        else:
            salt = uuid4().hex
            pass_hash = sha512(bytes(salt + pass1, 'utf-8')).hexdigest()

            hash_salt_comb = '$6$' + salt + '$' + pass_hash

            db.begin()
            db['users'].update({'username': username, 'p_hash': hash_salt_comb}, ('username'))
            db.commit()
            data['success'] = True

    return gen_page(os.path.join(DIR_TEMPLATES, 'changepass.html'), data)


@ui.route('/delete/<fileid>')
@ui.post('/delete/<fileid>')
@require_web_login
def ui_delete_file(fileid, username):
    """
    Webpage to allow users to delete files
    """
    data = {'deleted': False,
            'warning': False,
            'fileid': fileid}

    file_info = api_file_info(fileid)
    data['pastetitle'] = file_info['title']

    if username != file_info['user'] and username != ui.config['paste.admin']:
        abort(401, 'You aren\'t allowed to delete this file!')

    if request.method == 'POST':
        delete = request.forms.getunicode('delete')

        if delete:
            data['deleted'] = True

            delete_paste(fileid)
        else:
            redirect('/user')
    elif request.method == 'GET':
        data['warning'] = True
    else:
        pass

    return gen_page(os.path.join(DIR_TEMPLATES, 'deletefile.html'), data)


@ui.route('/logout')
@require_web_login
def end_login_session(username, forced=False):
    """
    End's a user's login session. Also removes their login cookie.

    forced - whether or not the login was forced (called from another method)
    """
    # Get username of user, delete cookie, and remove session from database
    response.delete_cookie('login_session')
    remove_session(username=username)

    if not forced:
        redirect('/login')


@ui.route('/uploadcombined')  # temporary redirect for fixing links
def upload_redirect():
    redirect('/upload')


@ui.route('/upload')
def web_upload_page():
    page_data = {}
    isFile = request.query.file

    if isFile:
        page_data['isFile'] = True
    else:
        page_data['isFile'] = False

    username = _get_session_username()
    if username:
        page_data['username'] = username

    return gen_page(os.path.join(DIR_TEMPLATES, 'upload.html'), page_data)


@ui.post('/upload')  # Combined upload page
def web_upload_post():
    username = check_login_session() or 'anonymous'

    output = api_upload(web_ui=True, username=username)

    if 'Success' in output:
        key = output.split(':')[1].strip()

        redirect('/p/{}'.format(key))


@ui.route('/register')
@ui.post('/register')  # Receiving registration data
def ui_register():
    """
    Register a user with the web frontent ui
    """
    if check_login_session():
        redirect('/user')

    reg_data = {'success_text': None,
                'error_text': None}

    if request.method == 'POST':  # see if we need to verify registration
        username = request.forms.getunicode('username')
        password = request.forms.getunicode('password')
        password_confirm = request.forms.getunicode('password-confirm')
        captcha_answer = request.forms.getunicode('captcha-challenge')
        captcha_id = request.cookies.getunicode('captcha_id')
        email = request.forms.getunicode('email') or None

        captcha_result = check_captcha(captcha_id, captcha_answer)

        if not sanitize(username):
            reg_data['error_text'] = """Usernames may only contain the """ \
                                     """following characters: {}""".format(ALLOWED_CHARS)
        elif len(username) < 2 or len(username) > 32:
            reg_data['error_text'] = 'Your username\'s length must be between 2 and 32 characters inclusive.'
        elif password != password_confirm:
            reg_data['error_text'] = "Those passwords don't match. Please try again."
        elif not captcha_result:
            reg_data['error_text'] = "Sorry, but you failed the captcha, please try again."
        elif len(password) <= 6:
            reg_data['error_text'] = 'Password too weak. Please try using more than 6 characters.'
        elif len(password) > 128:
            reg_data['error_text'] = """Your password is really long! Try again with a password """\
                                     """that is 128 characters or fewer."""
        else:
            # Check if username already exists
            taken = False
            if len(db['users']) > 0:
                users = db['users'].table
                query = users.select(users.c.username.ilike('%' + username + '%'))
                result = db.query(query)

                # See if there are any results
                for row in result:
                    taken = True
                    break

            if taken:
                reg_data['error_text'] = 'Sorry, but the username {} is already taken.'.format(username)
            elif username.lower().strip() == 'anonymous':
                reg_data['error_text'] = 'You are legion. You cannot register with "anonymous".'
            elif username.lower().strip() == 'root':
                reg_data['error_text'] = 'You must be a superuser to do that ;)'
            elif email and not ('@' in email):  # There are better ways to do this, but whatever
                reg_data['error_text'] = 'That email is invalid. Please try again.'
            else:
                salt = uuid4().hex
                p_hash = sha512(bytes(salt + password, 'utf-8')).hexdigest()

                # shadow password format
                hash_salt_comb = '6$' + salt + '$' + p_hash

                joined = int(time.time())

                db.begin()
                db['users'].insert({'username': username,
                                    'joined': joined,
                                    'p_hash': hash_salt_comb,
                                    'email': email,
                                    'views': 0,
                                    'bytes': 0,
                                    'totalpastes': 0,
                                    'activepastes': 0})
                db.commit()

                print('Registered user: {}'.format(username))
                reg_data['success_text'] = username

                # Log user in and stuff
                start_login_session(username)
                reg_data['username'] = username

    reg_data['captcha_data'] = captcha_provider()

    return gen_page(os.path.join(DIR_TEMPLATES, 'register.html'), reg_data)


def captcha_provider():
    """
    Generate random CAPTCHA using skimpyGimpy, give use CAPTCHA ID Cookie, and
    store CAPTCHA credential in database.
    """
    # Generate a random string 4 characters long
    pass_phrase = ''.join(SECRAND.choice(string.ascii_uppercase + string.digits) for _ in range(4))

    # Yes; use MD5 because the cookie doesn't have to be cryptographically secure.
    captcha_cookie_id = md5(bytes(str(uuid4()), 'utf-8')).hexdigest()

    # If there are already 10 sessions for creating cookies, expire the first session
    if len(CAPTCHA_SESSIONS) >= 10:
        _ = CAPTCHA_SESSIONS.pop(0)

    CAPTCHA_SESSIONS.append((captcha_cookie_id, pass_phrase))

    # Generate CAPTCHA with skimpyGimpy
    captcha_gen = skimpyAPI.Pre(pass_phrase, speckle=0.33, scale=1.33, color='#fff')
    captcha_test = captcha_gen.data()

    response.set_cookie('captcha_id', captcha_cookie_id)
    return captcha_test


@ui.route('/login')
@ui.post('/login')
def ui_login():
    """
    Login for UI
    """

    if check_login_session():
        redirect('/user')

    login_data = {'failure': False,
                  'redirect': None}

    redirect_url = request.query.redirect

    if request.method == 'POST':  # see if we need to verify registration
        username, password = get_login_creds()

        if check_login(username, password, anon=False):
            if request.forms.getunicode('remember'):
                start_login_session(username, remember=True)
            else:
                start_login_session(username)

            if request.forms.getunicode('redirect'):
                redirect(request.forms.getunicode('redirect'))
            else:
                redirect('/user')
        elif username.lower() == 'anonymous':
            login_data['failure'] = 'You cannot login as anonymous! Nice try though.'
        else:
            login_data['failure'] = """Oh noes! That username/password """ \
                                    """combination isn't recognized!"""

    if redirect_url:
        login_data['redirect'] = request.query.redirect

        if request.method == 'GET':
            login_data['failure'] = 'You need to log in before using that feature!'

    return gen_page(os.path.join(DIR_TEMPLATES, 'login.html'), login_data)


@ui.route('/p/<paste>')
def get_ui_paste(paste):
    # Get file_data first to handle 404s
    file_data = api_file_info(paste)

    if request.method == 'GET':  # Fix for HEAD to inflate view count
        increment_view_count(paste)

    file_data['short_key'] = paste
    file_data['file_internal_src'] = os.path.join(DIR_FILES, paste)
    file_data['file_external_src'] = '/api/file/{}'.format(paste)

    if not file_data['title']:
        file_data['title'] = 'Untitled Paste'

    return gen_page(os.path.join(DIR_TEMPLATES, 'paste.html'), file_data)


@ui.route(path=('/legal/<page>', '/legal', '/legal/'))
def ui_legal(page=None):
    """
    Combined pages for Terms of Service, Privacy Policy, AGPL, and 
    Hydra License.
    """
    if page == 'privacy':
        return gen_page(os.path.join(DIR_TEMPLATES, 'privacy.html'), dict())
    elif page == 'tos':
        return gen_page(os.path.join(DIR_TEMPLATES, 'tos.html'), dict())
    elif page == 'agpl':
        return gen_page(os.path.join(DIR_TEMPLATES, 'license-agpl.html'), dict())
    else:
        return gen_page(os.path.join(DIR_TEMPLATES, 'legal.html'), dict())


def gen_page(template_path, data=None):
    """
    Generates a page with the headers and footers, along with the data dictionary given

    template_path - abosolute path of the template to use
    data - any page-specific data that needs to be given to the page
        Optional Keys:
            username - username of logged in user
    """
    if not data:
        data = dict()

    data['top'] = os.path.join(DIR_TEMPLATES, 'top.html')
    data['bottom'] = os.path.join(DIR_TEMPLATES, 'bottom.html')
    data['footer'] = os.path.join(DIR_TEMPLATES, 'footer.html')
    data['stylesheets'] = tuple()
    data['scripts'] = tuple()
    data['og_image'] = '/static/favicon.ico'
    data['card_type'] = 'summary'
    data['user_is_admin'] = False
    data['git_hash'] = GIT_HASH
    data['git_repo'] = GIT_REPO
    data['git_commit_url'] = GIT_COMMIT_URL

    if 'username' not in data:
        data['username'] = _get_session_username() or None

    if data['username'] == ui.config['paste.admin']:
        data['user_is_admin'] = True

    if ALERT:
        data['alert'] = ALERT

    # Standard date time
    if 'time' in data:
        mod_time = data['time']

        time_diff = int(time.time()) - mod_time
        suffix = None
        time_unit = 0

        # Fast natural date implementation
        if time_diff <= 1 or time_diff <= 60:
            time_unit = time_diff  # Diff in seconds

            if time_diff == 1:
                suffix = 'second'
            else:
                suffix = 'seconds'
        elif time_diff > 60 and time_diff < 3600:
            # 60 secs/min
            time_unit = time_diff // 60

            if time_diff < 120:
                suffix = 'minute'
            else:
                suffix = 'minutes'
        elif time_diff >= 3600 and time_diff < 86400:
            # 60^2 sec/hr
            time_unit = time_diff // (60 * 60)

            if time_diff < 7200:
                suffix = 'hour'
            else:
                suffix = 'hours'
        else:
            # 60^2 * 24 sec/day
            time_unit = time_diff // (60 * 60 * 24)

            suffix = 'days'

        data['time'] = '{} {} ago'.format(time_unit, suffix)

    data['start_time'] = time.time()

    return template(template_path, data)


@ui.route('/')  # Index Page
@ui.route('/<filename>')  # Favicon.ico workaround
@ui.route('/static/<filename:path>')  # All other static files
def static_route(filename=None):
    """
    Route files in DIR_TEMPLATES when using --dev mode.

    Route homepage whenever using production setup.

    Also routes the index file (found in `/`)
    """
    if not filename:  # Frontpage
        return gen_page(os.path.join(DIR_TEMPLATES, 'frontpage.html'))
    elif filename == 'favicon.ico':  # Favicon workaround
        return static_file(filename, root=DIR_TEMPLATES)
    elif filename == 'robots.txt':
        return static_file(filename, root=DIR_TEMPLATES)
    else:
        return static_file(filename, root=DIR_TEMPLATES)


def prune_files(interval, _db):
    """
    Checks which files have expired given int(interval) as a second-format cooldown.
    """
    while True:
        time.sleep(interval)
        print('Cleaning shit up')
        now = int(time.time())
        try:
            # This is the fastest way to extract all files' information from the DB,
            # so the transaction is unlikely to get interrupted and crash!
            all_files = [fil for fil in _db['files'].all()]
        except:
            continue

        for _file in all_files:
            expires = int(_file['expires'])
            if expires <= now and expires:
                delete_paste(safeurl.num_encode(_file['key']))

        print('Shit cleaned up')


@ui.route(path=('/coffee', '/coffee/', '/coffee/<coffee_type>'))
def brew_coffee(coffee_type='any'):
    abort(418, 'Cannot brew {} coffee; can only brew tea.'.format(coffee_type))


def error_pages(code):
    abort(int(code), 'This is a manually triggered error.')


@ui.route('/doc')
@ui.route('/doc/')
def redirect_docs():
    return redirect('/doc/index.html')


@ui.route('/doc/<path:path>')
def serve_docs(path):
    '''
    Serve up that delicious documentation
    '''
    return static_file(path, os.path.join(ROOT, 'doc'))


if __name__ == '__main__':
    if eval(ui.config['debug'].strip()):
        ROOT = os.getcwd()
        print('\nRunning in developer mode at `{}`'.format(ROOT))
        ui.route('/error/<code>', callback=error_pages)
        print('View error pages at "/error/<httpcode>"; eg: /error/404')

    try:
        print('\nOpening database `{}`...'.format(ui.config['paste.database']))
        db = dataset.connect('sqlite:///{}'.format(ui.config['paste.database']))
    except:
        print('Unable to open database. Exiting program...')
        quit()
        pass

    # Create the database tables if they don't exist
    for table_type in ['files', 'users', 'captcha', 'session']:
        if table_type not in db.tables:
            db.create_table(table_type)

    # check to make sure `files` folder exists, if not, then create it.
    if not os.path.exists(DIR_FILES):
        os.makedirs(DIR_FILES)

    # Starting the prune_files(interval) thread.
    t_prune = Thread(target=prune_files, args=(CLEAN_INTERVAL, db))
    t_prune.start()

    run(ui, host=ui.config['host'], port=int(ui.config['port']))
